/*
	jGrouse framework
	Copyright 2007 by Denis Riabtchik 
	All rights reserved.
	
	Licensed under modified BSD license. See license.txt for details
*/


/**
	Module with base jGrouse widget
	@module jgrouse.widgets.Widget
	@requires jgrouse.dom.Element
	@requires jgrouse.dom.Event
	@requires jgrouse.util.PropertyChangeSupport
	@requires jgrouse.util.Sets
	@requires jgrouse.util.MvcDriver
	@requires jgrouse.util.Maps
*/
jgrouse.module(
{
	name : 'jgrouse.widgets.Widget',
	requires : ['jgrouse.dom.Element', 
				'jgrouse.dom.Event', 
				'jgrouse.util.PropertyChangeSupport', 
				'jgrouse.util.Sets',
				'jgrouse.util.MvcDriver',
				'jgrouse.util.Maps', 'jgrouse.util.TaskManager'],
	imports : ['jgrouse.util.PropertyChangeSupport', 'jgrouse.util.PropertyChangeEvent', 
		'jgrouse.dom.Element', 'jgrouse.dom.Event', 'jgrouse.util.SimpleMap', 'jgrouse.string.*'],
	body : function(embed)
	{
		eval(embed);
		
		/**
		 * Structure describing dimensions of widget, in px
		 * @struct jgrouse.widgets.Dimensions
		 */
		/**
		 * Width in px
		 * @variable width
		 */
		/**
		 * Height in px
		 * @variable height 
		 */
		
		/**
			Base class for all jGrouse widgets. <br/>
			
			@class jgrouse.widgets.Widget
			@extends jgrouse.BaseObject
			@implements jgrouse.util.PropertyChangeSupport
		*/		
		jgrouse.define('jgrouse.widgets.Widget', 'jgrouse.BaseObject', function(salt)
		{
			eval(salt);

			
			var res = {
				/**
					@constructor initialize
					@param {Object} attributes - structure with attributes of widget
					@param {Object} styles - structure with widget's CSS styles
				*/
				initialize : function(attributes, styles)
				{
					_super(this, 'initialize');
					/**
						Attributes of widget that should be set on DOM node
						@variable {Object} _attributes
					*/
					this._attributes = attributes? attributes : {};
					/**
						Styles of widget that should be set on DOM node
						@variable {Object} _styles
					*/
					this._styles = styles? styles : {};
					/**
						Root DOM element for widget
						@variable {DOMElement} _domElement 
					*/
					this._domElement = null;
					/**
						Reference point after which widget's DOM element is inserted
						@variable {DOMElement} _refPoint 
					*/
					this._refPoint = null;
					/**
						PropertyChangeSupport where listeners are being registered
						@variable {jgrouse.util.PropertyChangeSupport} _pcs 
					*/
					this._pcs = new PropertyChangeSupport(this);
					/**
						Container that owns the widget
						@variable {jgrouse.widgets.Container} _owner 
					*/
					this._owner = null;
					/**
						Indicator if the widget is showing in document
						@variable {boolean} _isShowing
					*/
					this._isShowing = false;
					/**
						Name of tag that is used to render widget's DOM element
						@variable {String} _tagName 
					*/
					this._tagName = 'div';
					if (!this._attributes.id)
					{
						this._attributes.id = jgrouse.widgets.nextId();
					}
					/**
						Bindings between widget's properties and model's properties
						@variable {String} ?
						@see setBindings
					*/
					this._bindings = null;
					/**
						MVC driver to which the widget was binded
						@variable {jgrouse.util.MvcDriver} _mvcDriver 
					*/
					this._mvcDriver = null;
					/**
						Hash with information regarding what kind of events were enabled
						@variable {hash} _enabledEvents 
					*/
					this._enabledEvents = {};
					/**
						Value that is being passed as parameter for triggerClick property
					*/
					this._clickValue = undefined;
					/**
						List of events for which default handling should be suppressed 
						@variable {Object} _suppressDefault
					*/
					this._suppressDefault = {};
					jgrouse.widgets._widgetInstances.put(this.__serialObjId, this);
					/**
						Map of spices added to the widget. Keys are spices names, values are spice instances
						@variable {jgrouse.util.SimpleMap} _spices
					*/
					this._spices = new SimpleMap();
					
					/**
					 * Dimensions of widget in pixels
					 * @variable {jgrouse.widgets.Dimensions} _cachedBounds
					 */
					this._cachedBounds = null;
				},
				
				/**
					Apply all attributes and styles to rendered DOM elements
					@function applyAttributes
				*/
				applyAttributes : function()
				{
					if (this._domElement)
					{
						Element.setAttributes(this._domElement, this._attributes);
						Element.setStyles(this._domElement, this._styles);
					}
				},
				
				/**
					Enable event handling by widget. Enabling of event &lt;eventName&gt 
					is done by binding method called on&lt;EventName&gt to widget's dom as event 
					listener
					@function enableEvent
					@param {String} eventName - name of event to be enabled
					@see enableEvent
				*/
				enableEvent : function(eventName)
				{
					var arr = typeof eventName == 'array'? eventName : [eventName];
					var athis = this;
					jgrouse.forEach(arr, function(ev)
					{
						if (!athis._enabledEvents[ev])
						{
							var name = 'on' + jgrouse.string.upperFirst(ev);
							var func = jgrouse.bindAsEventListener(athis, function(event)
							{
								this[name](event);
								if (this._suppressDefault[ev])
								{
									Event.cancelEvent(event);
								}
							});
							athis._enabledEvents[ev] = func;
							if (athis._domElement)
							{
								Event.addListener(athis._domEvent, ev, func, true);
							}
						}
					});
				},
				
				setSuppressDefault : function(values)
				{
					var arr = values.split(';');
					for (var i = 0; i < arr.length; i++)
					{
						this._suppressDefault[arr[i]] = true;
					}
				},
				
				_bindEnabledEvents : function()
				{
					var athis = this;
					jgrouse.forEach(this._enabledEvents, function(func, eventName)
					{
						Event.addListener(athis._domElement, eventName, func, true);
					})
				},
				
				
				/**
					Remove all event listeners from widget's DOM element
					@function clearEventListeners
				*/
				clearEventListeners : function()
				{
					Event.removeElementListeners(this._domElement);
				},
				
				/**
					Disable event processing by the widget. Disabling from event &lt;eventName&gt; 
					is done by unbinding of widget's method on&lt;EventName&gt; from DOM.
					@function disableEvent
					@param {String} eventName
					@see enableEvent
				*/
				disableEvent : function(eventName)
				{
					if (this._enabledEvents)
					{
						var func = this._enabledEvents[eventName];
						if (func)
						{
							if (this._domElement)
							{
								this.removeEventListener(this._domElement, eventName, func, true);
							}
							delete this._enabledEvents[eventName];
						}
					}
				},
				
				/**
					Set mappings between view's properties and corresponding model properties.
					Bindings are contained in string in format viewProp1:modelProp1[;viewProp2:modelProp2]
					@function setBindings
					@param {String} bindings - string with bindings that should be set
				*/
				setBindings : function(bindings)
				{
					this._bindings = bindings;
				},
				
				/**
					Unbind all view's properties from model properties
					@function unbind
					@see bind
				*/
				unbind : function()
				{
					if (this._mvcDriver)
					{
						this._mvcDriver.unbind(this);
						var athis = this;
						this._spices.forEach(function(spice)
						{
							athis._mvcDriver.unbind(spice);
						});
						if (this._bindings)
						{
							var pairs = this._bindings.split(';');
							for (var i = 0; i < pairs.length; i++)
							{							
								var viewProp = trim(pair[0]);
								if (viewProp.charAt(0) == '#')
								{
									var modelProp = trim(pair[1]);
									this._mvcDriver.unbind(jgrouse.util.TaskManager, modelProp, viewProp.substring(1));									
								}
							}
						}
					}
					this._mvcDriver = null;
				},
				
				/**
					Bind view's properties to model's properties
					@function bind
					@param {jgrouse.util.MvcDriver} mvcDriver - MVC Driver that glues model, view and controller
					@see unbind
				*/
				bind : function(mvcDriver)
				{
					this.unbind();
					this._mvcDriver = mvcDriver;
					if (this._bindings)
					{
						var pairs = this._bindings.split(';');
						for (var i = 0; i < pairs.length; i++)
						{
							var pair = pairs[i].split(':');
							var viewProp = trim(pair[0]);
							var modelProp = trim(pair[1]);
							if (!viewProp)
							{
								viewProp = 'value';
							}
							if (viewProp.charAt(0) == '#')
							{
								var prop = viewProp.substring(1);
								this._mvcDriver.bind(jgrouse.util.TaskManager, modelProp, viewProp.substring(1));
							} 
							else if (viewProp.charAt(0) == '$')
							{
								var props = viewProp.split('.');
								var member = props[0];
								var vp = props[1];
								var spice = this._spices.get(member);
								this._mvcDriver.bind(spice, vp, modelProp);								
							} else
							{
								this._mvcDriver.bind(this, viewProp, modelProp);
								if (viewProp.indexOf('trigger') == 0)
								{
									var base = viewProp.substr(7);
									var methodName = 'on' + upperFirst(base);
									if (this[methodName])
									{
										this.enableEvent(base.toLowerCase());
									}
								}
							} 
						}
					}
				},
				
				getMvcDriver : function()
				{
					return this._mvcDriver;
				},
				
				_translateAttrName : function(attr)
				{
					return (attr == 'class')? 'className' : attr; 
				},
				
				/**
					Set widget's attribute. If widget already has DOM element, attribute would be 
					also added to DOM node
					@function setAttribute
					@param {String} attrName - name of attribute to set
					@param {String} attrValue - value to be set for attribute
				*/
				setAttribute : function(attrName, attrValue)
				{
					this._attributes[attrName] = attrValue;
					if (this._domElement)
					{
						Element.setAttribute(this._domElement, attrName, attrValue);
					}
				},
				
				/**
					Get widget's attribute.
					@function getAttribute
					@param {String} attrName
				*/
				getAttribute : function(attrName)
				{
					return this._attributes[attrName];
				},
				
				/**
					Add CSS style(s) to widget. If widget already has rendered DOM element then styles
					would be applied immediately to that DOM element.
					@function addStyles
					@paramset add one style
					@param {String} styleName - name of CSS style
					@param {String} styleValue - value of CSS style
					@paramset add several styles
					@param {hash} styles - hashmap where keys are names of CSS styles 
					and values are values for those styles
				*/
				addStyles : function(styleName, styleValue)
				{
					if (arguments.length == 2)
					{
						this._styles[styleName] = styleValue;
						if (this._domElement)
						{
							Element.addStyle(this._domElement, 	styleName, styleValue);
						}
					}
					else
					{
						var athis = this;
						jgrouse.forEach(styleName, function(value, name)
						{
							athis.addStyles(name, value);								
						});
					}
				},
				
				/**
					Add HTML class to widget. If widget already has rendered DOM element, then 
					class would be added to that DOM element
					@function addClassName
					@param {String} className
					@see removeClassName
				*/
				addClassName : function(className)
				{
					var s = this.getAttribute('class');
					if (s)
					{
						s = s.split(' ');
					}
					else
					{
						s = [];
					}
					if (jgrouse.search(s, className) < 0)
					{
						//s.push(className);
						s.splice(0, 0, className);
						var v = s.join(' ');
						this.setAttribute('class', v);
					}
				},
				
				/**
					Remove HTML class from widget. If widget already has root DOM element, then
					class would be also removed from that DOM element
					@function removeClassName
					@param {String} className - class name to remove
					@see addClassName
				*/
				removeClassName : function(className)
				{
					var s = this.getAttribute('class');
					if (s)
					{
						s = s.split(' ');
						var i = jgrouse.search(s, className);
						if (i >= 0)
						{
							s.splice(i, 1);
							this.setAttribute('class', s.join(' '));
						}
					}
				},
				
				/**
					Get widget's DOM element. If it has not been created yet, then it would be 
					created and rendered
					@function {DOMElement} getDomElement
					@returns widget's DOM element
				*/
				getDomElement : function()
				{
					if (!this._domElement)
					{
						this.render();
					}
					return this._domElement;
				},
				
				/**
					Add spice to widget
					@function {Spice} addSpice
					@param {String} spiceName - name of the spice added to widget
					@param {String} spiceAlias - alias under which spice's class is registered
					@return created spice
					@see Spice
				*/
				addSpice : function(spiceName, spiceAlias)
				{
					var spiceClass = jgrouse.widgets.getSpiceClass(spiceAlias);
					if (spiceClass == null)
					{
						throw 'Cannot find class for spice ' + spiceAlias;
					}
					var spice = new spiceClass(this);
					this._spices.put(spiceName, spice);
					return spice;
				},
				
				/**
					Get named spice from widget
					function {Spice} getSpice
					@param {String} spiceName
				*/
				getSpice : function(spiceName)
				{
					return this._spices.get(spiceName);
				},
				
				_removeOneSpice : function(spice)
				{
					if (spice)
					{
						this._mvcDriver.unbind(spice);
						spice.destroy();
					}
				},
				
				/**
					Remove spice from widget
					@function removeSpice
					@param {String} spiceName
				*/
				removeSpice : function(spiceName)
				{
					var spice = this._spices.remove(spiceName);
					this._removeOneSpice(spice);
				},
				
				
				/**
					Render widget as DOM element(s).
					@function render
				*/
				render : function()
				{
					this._domElement = document.createElement(this._tagName);
					this._domElement._jgwidget = this;
					this.applyAttributes();
					this._bindEnabledEvents();
					this.renderSpices();
				},
				
				/**
					Render all spices. Allows spices to attach to widget's dom objects and register 
					necessary listeners, etc. 
					@function renderSpices
				*/
				renderSpices : function()
				{
					this._spices.forEach(function(spice)
					{
						spice.render();
					});
				},
				
				/**
					Check if widget is showing in document
					@function isShowing
					@returns true if widget is reacheable from the document's root and display is not 'none'
				*/
				isShowing : function()
				{
					return this._isShowing;
				},
				
				/**
					Set widget's owner component
					@function setOwner
					@param {jgrouse.widgets.Container} owner 
				*/
				setOwner : function(owner)
				{
					this._owner = owner;
				},
				
				/**
					Add widget's generated content to document's DOM after given reference point
					@function add
					@param {DOMElement} parentDom - DOM element into which widget's DOM should be inserted
					@param {DOMElement} refPoint - reference point after which the element should be inserted. 
					Optional. If omitted then widget's DOM is added to the end of owner's DOM
				*/
				add : function(parentDom, refPoint)
				{
					if (parentDom)
					{
						if (refPoint)
						{
							parentDom.replaceChild(this.getDomElement(), refPoint);
						}
						else
						{
							parentDom.appendChild(this.getDomElement());
						}
						this._refPoint = refPoint;
					}
					this.resetShowing();
				},
				
				setDisplayed : function(isDisplayed)
				{
					this.addStyles('display', isDisplayed? '' : 'none');
					this.resetShowing();
				},
				
				
				/**
					Recalculate visibility status for widget
					@function resetShowing
				*/
				resetShowing : function()
				{
					var showing = !!this._domElement && 
						this._owner? this._owner.isShowing() : Element.isShowing(this._domElement);
					if (showing)
					{
						showing &= this._styles.display != 'none';
					}
					this.setShowing(showing);
				},
				
				
				resetBounds : function()
				{
					if (this.isShowing())
					{
						var oldHeight = 0;
						var oldWidth = 0;
						var bounds = this._cachedBounds;
						if (bounds)
						{
							oldHeight = this._cachedBounds.height;
							oldWidth = this._cachedBounds.width;
						}
						else
						{
							bounds = {};
							this._cachedBounds = bounds;
						}
						bounds.width = this._domElement.offsetWidth;
						bounds.height = this._domElement.offsetHeight;
						if (bounds.width != oldWidth || bounds.height != oldHeight)
						{
							if (this.layout)
							{
								this.layout();
							}
						}
					}
					else
					{
						this._cachedBounds = null;
					}
					
				},
			
				/**
					Set showing status for widget. If the status would be different from current,
					then all listeners would be notified that "showing" property had been changed
					@function setShowing
				*/
				setShowing : function(isShowing)
				{
					if (isShowing != this._isShowing)
					{
						this._isShowing = isShowing;
						this.resetBounds();
						this.firePropertyChange('showing', isShowing, !isShowing);
					}
				},
				
				/**
					Remove widget from parent and optionally dispose it.
					@function remove
					@param {boolean} doDispose - indicator if the widget should be disposed after it is 
					removed from parent
				*/
				remove : function(doDispose)
				{
					if (this._domElement)
					{
						var parentNode = this._domElement.parentNode;
						if (parentNode)
						{
							if (this._refPoint)
							{
								parentNode.replaceChild(this._refPoint, this._domElement);
							}
							else
							{
								parentNode.removeChild(this._domElement);
							}
							this._refPoint = null;
							this.setShowing(false);
						}
						if (doDispose)
						{
							this.dispose();
						}
					}
				},
				
				/**
					Dispose the widget and deallocate all resources associated with the widget
					@function dispose
				*/
				dispose : function()
				{
					this.disposeSpices();
					this.remove(false);
					if (this._domElement)
					{
						this.clearEventListeners();
						Element.disposeDom(this._domElement);
						this._domElement._jgwidget = null;
						this._domElement = null;
					}
					jgrouse.widgets._widgetInstances.remove(this.__serialObjId);
				},
				
				/**
					Dispose all widget's spices. Called as a part of widget's dispose method.
					@function disposeSpices
				*/
				disposeSpices : function()
				{
					this._spices.forEach(function(spice)
					{
						spice.dispose();
					});
				},
				
				/**
					Disables or enables widget
					@function setDisabled
					@param {boolean} value - true if widget should be disabled
				*/
				setDisabled : function(value)
				{
					this.setAttribute('disabled', value);
				},
				
				/**
					Set the value that should be passed as parameter for triggerClick notifications
					@function setClickValue
					@param {String} value - value to be passed as newValue
				*/
				setClickValue : function(value)
				{
					this._clickValue = value;
				},
				
				getClickValue : function()
				{
					return this._clickValue;
				},
				
				/**
					Handler for <b>click</b> events. Fires <b>triggerClick</b> property
					@function onClick
					@param {DOMEvent} event
					
				*/
				onClick : function(event)
				{
					this.firePropertyChange('triggerClick', this.getClickValue());
				},
				
				/**
					Sets name of tag to be used for rendering of main DOM node
					@function setTagName
					@param {String} tagName - tag name to be used (div, span, etc)
				*/
				setTagName : function(tagName)
				{
					this._tagName = tagName;
				}
				
			};
			
			jgrouse.delegate(res, '_pcs', PropertyChangeSupport.prototype);
			return res;
		});
		
		/**
			Spices are components that enhance default behavior of widgets. For example, a spice could add
			a "hover" effect to a widget or to a DOM node in a widget.
			@class jgrouse.widgets.Spice
			@extends jgrouse.BaseObject
		*/
		jgrouse.define('jgrouse.widgets.Spice', 'jgrouse.BaseObject', function(embed)
		{
			eval(embed);
			
			var res = {
				/**
					@constructor initialize
					@param {jgrouse.widgets.Widget} widget - owner of the spice
				*/
				initialize : function(widget)
				{
					this._widget = widget;
					/**
						Member of widget (named DOM node) to which the spice is attached. When not specified,
						it is assumed that spice is operating on the widget itself.
						@variable {String} _memberName
					*/
					this._memberName = null;
					this._pcs = new PropertyChangeSupport(this);
				},
				
				/**
					Set the name of member to which the spice is attached
					@function setNodeMember
					@param {String} memberName
					@see _memberName
				*/
				setNodeMember : function(memberName)
				{
					this._memberName = memberName;
				},
				
				/**
					Get the DOM node to which the spice is attached.
					@function {DOMElement} getNode
					@returns null if spice is attached to widget itself
				*/
				getNode : function()
				{
					return this._memberName? this._widget[this._memberName] : null;
				},
				
				/**
					Get owner of the spice
					@function {jgrouse.widgets.Widget} getWidget
				*/
				getWidget : function()
				{
					return this._widget;
				},
	
				/**
					Perform necessary actions when the widget is being rendered
					@function render
				*/				
				render : function()
				{
				},

				/**
					Release all necessary reference (event listeners, etc) when the widget is being disposed
					@function dispose
				*/				
				dispose : function()
				{
				},
				
				addClassName : function(className)
				{
					var element = this.getNode();
					if (element)
					{
						Element.addClassName(element, className);
					}
					else
					{
						this.getWidget().addClassName(className);
					}				
				},
				
				removeClassName : function(className)
				{
					var element = this.getNode();
					if (element)
					{
						Element.removeClassName(element, className);
					}
					else
					{
						this.getWidget().removeClassName(className);
					}				
				},
				
				/**
				 * Layout content of widget after resize
				 * @function layout
				 */
				layout : function()
				{
					
				}
			}
			
			jgrouse.delegate(res, '_pcs', PropertyChangeSupport.prototype);
			return res;
		});
		
	},
	
	
	
	postInit : function(embed)
	{
		eval(embed);
		
		var widgestry = new jgrouse.util.SimpleMap();
		
		var spices = new jgrouse.util.SimpleMap();
		
		/**
			@namespace jgrouse.widgets
		*/
		
		/**
			Associate widget's alias with widget's class
			@function registerWidget
			@param {String} widgetName - widget's alias
			@param {String} widgetClassName - name of widget's class
		*/
		jgrouse.widgets.registerWidget = function(widgetName, widgetClassName)
		{
			widgestry.put(widgetName, widgetClassName);
		}
		
		/**
			@function registerSpice
			@param {String} spiceName - spice's alias
			@param {String} spiceClassName - spice's class name
		*/
		jgrouse.widgets.registerSpice = function(spiceName, spiceClassName)
		{
			spices.put(spiceName, spiceClassName);
		}
		
		/**
			Get widget's class by widget's alias
			@function {String} getWidgetClass
			@param {String} widgetType - widget's alias
		*/
		jgrouse.widgets.getWidgetClass = function(widgetType)
		{
			var name = widgestry.get(widgetType);
			return (name)? jgrouse.resolveName(name) : null;
		}
		
		/**
			Get spice's class by alias
			@function getSpiceClass
			@param {String} spiceAlias
		*/
		jgrouse.widgets.getSpiceClass = function(spiceAlias)
		{
			var name = spices.get(spiceAlias);
			return (name)? jgrouse.resolveName(name) : null;
		}
		

		var seed = 'jg' + String((new Date()).getTime()) + '-';
		var counter = 0;
		
		function nextId()
		{
			return seed + (counter ++);
		};

		
		/**
			Generate unique identifier that could be used as an ID for DOM element.
			@function {String} nextId
		*/
		jgrouse.widgets.nextId = nextId;
		
		/**
			Map containing all widget instances in the system
			@variable jgrouse.widgets._widgetInstances
		*/
		jgrouse.widgets._widgetInstances = new SimpleMap();
		
		jgrouse.widgets.__disposeHook = function()
		{
			jgrouse.widgets._widgetInstances.forEach(function(widget)
			{
				widget.dispose();
			});
			jgrouse.widgets._widgetInstances.clear();
		}
		
		jgrouse.addUnloadHook(jgrouse.widgets.__disposeHook);
	}
});